Skip to content

Events Overview

Understanding the event lifecycle and when the agent is done

Every agent interaction emits a stream of events that describe exactly what's happening: when the agent starts thinking, calls tools, generates text, and finishes. Understanding this event system is essential for building responsive applications.

The Two-Level Turn Hierarchy

** CRITICAL CONCEPT:** There are TWO levels of turns, and knowing the difference is essential for building correct UIs.

Message Turn vs Agent Turn

MESSAGE TURN (entire user interaction)
├── MessageTurnStartedEvent
├── User: "What's the weather in Paris?"

├── AGENT TURN 1 (first LLM call)
│   ├── AgentTurnStartedEvent
│   ├── LLM responds: "I'll check the weather"
│   ├── ToolCallStartEvent: get_weather
│   └── AgentTurnFinishedEvent

├── Tool executes: get_weather("Paris") → "22°C, sunny"
│   └── ToolCallResultEvent

├── AGENT TURN 2 (second LLM call)
│   ├── AgentTurnStartedEvent
│   ├── TextDeltaEvent: "The"
│   ├── TextDeltaEvent: " weather"
│   ├── TextDeltaEvent: " is 22°C and sunny"
│   └── AgentTurnFinishedEvent

└── MessageTurnFinishedEvent ← THIS is when the agent is DONE!

Key Insights

  • Message Turn: The ENTIRE conversation from user input to final response

    • Starts: MessageTurnStartedEvent
    • Ends: MessageTurnFinishedEventUse this for UI state!
  • Agent Turn: A single LLM API call (there can be many per message turn)

    • Starts: AgentTurnStartedEvent
    • Ends: AgentTurnFinishedEventUsually ignore these

** Common Mistake:** Using AgentTurnFinishedEvent to stop loading spinner causes the UI to show "done" too early while the agent is still working!

** Correct:** Always use MessageTurnFinishedEvent to know when the agent is completely finished.

Event Flow

All events flow through the Agent.RunAsync() async stream:

csharp
await foreach (var evt in agent.RunAsync(messages))
{
    // Events arrive here in real-time
    // Process them with pattern matching
}

The stream delivers events in chronological order as they occur, enabling real-time UI updates.

Event Categories

Events are organized into these categories:

1. Turn Lifecycle Events

Control the conversation flow:

  • MessageTurnStartedEvent - User message processing begins
  • MessageTurnFinishedEvent - Agent is done ← Use this!
  • MessageTurnErrorEvent - Unrecoverable error occurred
  • AgentTurnStartedEvent - Internal LLM call begins (usually ignore)
  • AgentTurnFinishedEvent - Internal LLM call ends (usually ignore)

2. Content Events

The agent's text response:

  • TextDeltaEvent - Streaming text chunks (accumulate these to build the response)
  • TextMessageStartEvent - Message boundary (optional, nice for polish)
  • TextMessageEndEvent - Message boundary (optional, nice for polish)

3. Reasoning Events

Extended thinking (Claude's internal reasoning):

  • ReasoningDeltaEvent - Streaming reasoning content
  • ReasoningMessageStartEvent - Reasoning begins
  • ReasoningMessageEndEvent - Reasoning ends

4. Tool Events

When the agent calls functions:

  • ToolCallStartEvent - Tool invocation begins (show "Calling calculator..." in UI)
  • ToolCallResultEvent - Tool execution complete (show result or error)
  • ToolCallArgsEvent - Streaming tool arguments (advanced)
  • ToolCallEndEvent - Arguments complete (advanced)

5. Bidirectional Events

Events that require a response from the user:

  • PermissionRequestEventPermissionResponseEvent - Ask user to approve tool execution
  • ClarificationRequestEventClarificationResponseEvent - Ask user for more info
  • ClientToolInvokeRequestEventClientToolInvokeResponseEvent - Execute tool on client

** CRITICAL:** These events require calling agent.SendResponseAsync() or the agent will hang until timeout!

6. Observability Events

Internal diagnostics (filter these out!):

  • 25+ internal events like MiddlewareProgressEvent, CircuitBreakerTriggeredEvent, etc.
  • All implement IObservabilityEvent marker interface
  • Always filter these out in user-facing code to prevent console spam

Basic Event Handling Pattern

Here's the fundamental pattern every application needs:

csharp
await foreach (var evt in agent.RunAsync(messages))
{
    switch (evt)
    {
        // Content streaming
        case TextDeltaEvent delta:
            Console.Write(delta.Text);
            break;

        // Reasoning
        case ReasoningDeltaEvent reasoning:
            Console.Write($"[Thinking: {reasoning.Text}]");
            break;

        // Tool execution
        case ToolCallStartEvent toolStart:
            Console.WriteLine($"\n[Calling: {toolStart.ToolName}]");
            break;

        case ToolCallResultEvent toolResult:
            Console.WriteLine($"[Result: {toolResult.Result}]");
            break;

        //   THIS is when to stop your loading spinner!
        case MessageTurnFinishedEvent:
            Console.WriteLine("\n✓ Agent finished");
            // In a web UI: setIsLoading(false), enableInput()
            break;

        // Error handling
        case MessageTurnErrorEvent error:
            Console.WriteLine($"\n✗ Error: {error.ErrorMessage}");
            break;

        // Permissions (requires SendResponseAsync!)
        case PermissionRequestEvent permission:
            var approved = PromptUser($"Allow {permission.FunctionName}?");

            // THIS LINE IS MANDATORY - don't forget it!
            await agent.SendResponseAsync(permission.PermissionId,
                new PermissionResponseEvent
                {
                    PermissionId = permission.PermissionId,
                    Approved = approved
                });
            break;
    }
}

Event Properties

All events inherit from AgentEvent and share these core properties:

csharp
public abstract record AgentEvent
{
    // Event routing
    public EventPriority Priority { get; init; }         // Immediate/Control/Normal/Background
    public EventDirection Direction { get; init; }       // Downstream/Upstream
    public string? StreamId { get; init; }               // For stream interruption

    // Nested agent tracking
    public AgentExecutionContext? ExecutionContext { get; init; }

    // Stream control
    public bool CanInterrupt { get; init; }              // Can be dropped on cancellation
    public long SequenceNumber { get; internal set; }    // Order of emission
}

Most applications only need to care about the event type itself. Advanced scenarios may use:

Common Beginner Mistakes

  1. Using AgentTurnFinishedEvent instead of MessageTurnFinishedEvent
  • Result: UI shows "done" while agent is still working
  • Fix: Always use MessageTurnFinishedEvent for UI state
  1. Handling PermissionRequestEvent but not calling SendResponseAsync()
  • Result: Agent hangs for ~30 seconds until timeout
  • Fix: Always call SendResponseAsync() for bidirectional events
  1. Forgetting to handle MessageTurnFinishedEvent
  • Result: Loading spinner never stops, input stays disabled
  • Fix: Always handle MessageTurnFinishedEvent to update UI state

What's Next

This overview covers the core concepts. For detailed guides:

Event Documentation

Getting Started

Released under the MIT License.